/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */

package simco.framework.gui.graph;

import java.awt.Color;
import java.awt.Graphics2D;

import simco.framework.gui.graph.base.GraphObject;
import simco.framework.gui.graph.iface.Connectable;
import simco.framework.gui.graph.supp.LineEdge;
import simco.framework.gui.graph.supp.PositionCoordinates;
import simco.framework.gui.graph.supp.ViewModeType;
import simco.framework.gui.utils.Configuration;

/**
 * Trida reprezentujici poskytovane rozhrani komponentami 
 * v simulaci. Kazde rozhrani je touto tridou vizualizovano
 * jako "lizatko". Trida dale zajistuje vykresleni pripojne
 * "packy" pro kazdou caru znazornujici vazvu pouzivani
 * rozgrani.
 *  
 * @author Matej Prokop, mythge@students.zcu.cz
 */
public class ProvidedInterface extends GraphObject
{   
	/**
	 * Konec cary smerujici k objektu poskytujici rozhrani
	 */
    private LineEdge edgeToOwner;

    /**
     * Maximalni velikost uhlu
     */
    public final int ANGLE_LIMIT = 360;

    /**
     * Relativni velikost prichytne packy vyjadrena ve stupnich 
     */
    public final int DOCK_SIZE = 24;

    /**
     * Rozmer opsaneho ctvrce (kolem kruhu znazornujici rozhrani)
     * pri minimalizovanem zobrazeni
     */
    public final int COMPRESED_SIZE = 34;
    
    /**
     * Rozmer opsaneho ctvrce (kolem kruhu znazornujici rozhrani)
     * pri standardnim zobrazeni
     */
    public final int DEFAULT_SIZE = 44;
    
    /**
     * Rozmer opsaneho ctvrce (kolem kruhu znazornujici rozhrani)
     * pri detailnim zobrazeni
     */
    public final int DETAIL_SIZE = 54;
    
    /**
     * Prumer kruznice, znazornujici rozhrani
     */
    private int innerSize;

    /**
     * Vzdalenost leveho horniho rohu ctvrce opsaneho okolo kruznice
     * znazornujici rozhrani a leveho horniho rohu ctvrce opsaneho
     * kolem kruznice, na kterou jsou vykreslovany packy pripojenych
     * vazeb.
     */
    private int innerCordDif;

    /**
     * Vytvori novou instanci rozhrani s pozadovanym id objektu.
     * 
     * @param objectId id objektu prirazene vytvarenemu rozhrani
     */
    public ProvidedInterface(long objectId)
    {
        super(objectId);

        innerSize = DEFAULT_SIZE;
        recomputeInnerCordDif();
    }

    /**
     * Vypocita vzdalenost leveho horniho rohu ctvrce opsaneho okolo kruznice
     * znazornujici rozhrani a leveho horniho rohu ctvrce opsaneho
     * kolem kruznice, na kterou jsou vykreslovany packy pripojenych
     * vazeb.
     */
    private void recomputeInnerCordDif()
    {
        // b je sirka ctverece, do ktereho je vepsana kruznice repreznetujici rozhrani
        int b =  (int) (Math.sqrt(Math.pow(this.innerSize, 2) + Math.pow(this.innerSize, 2)));
        /*
         * x je rozdil mezi rohem ctverece do ktereho je vepsana kruznice
         * reprezentujici rozhrani a ctevrce do ktereho je vepsana kruznice
         * reprezentujici jednotlive pripojene "packy"
         */
        int x = (int) (b / 2.0 - this.innerSize / 2.0);

        this.innerCordDif = x;
        this.width = height = b;    	
    }
    
    @Override
    protected void objectHasMoved(int xChange, int yChange)
    {
        for(LineEdge edge : attachedEdges)
        {
            computeLineIntersectionPoint(edge);
        }

        if(edgeToOwner != null)
        {
            edgeToOwner.setX(getMidX());
            edgeToOwner.setY(getMidY());
        }
    }

    @Override
    protected void newLineHasBeenAdded(LineEdge attachedEdge)
    {
        computeLineIntersectionPoint(attachedEdge);
    }

    /**
     * Pro zadany bod P na kruznici vypocte uhel, ktery svira primka
     * prochazejici bodem P a bodem S (stred krznice) s primkou prochazejici
     * bodem S a 'nejvychodnejsim' bodem kruznice. Uhel je orientovan po smeru
     * hodinovych rucicek.
     *
     * @param pointX souradnice x bodu P
     * @param pointY souradnice y bodu P
     * @return vypocitany uhel pro zadany bod
     */
    private double computeAngleOfPoint(double pointX, double pointY, Graphics2D g2)
    {
        // polemer kruzinice na, ktere lezi packy pripojenych car (vazeb)
        double r = width * 0.5;
        // vypocte uhel z rovnice: Px = Sx + r * cos(alfa)
        double angleX = Math.acos((pointX - getMidX()) / r);
        // vypocte uhel z rovnice: Py = Sy + r * sin(alfa)
        double angleY = Math.asin((pointY - getMidY()) / r);

        // konverzer z radianu na stupne
        angleX = Math.toDegrees(angleX);
        angleY = Math.toDegrees(angleY);

        double finalniUhel = angleX;
        /*
         * Vybere spravnou polaritu vysledky. Pokud je uhel vypocteny
         * ze slozky y zaporny, pak je nutne prohodit polaritu, uhlu
         * vypocteneho ze slozky x, aby mel vysledek spravnou polaritu.
         */
        if(Double.isNaN(angleX) || Double.isNaN(angleY) )
        {
            return Double.NaN;
        }
        else if(angleY > 0.01)
        {
            finalniUhel *= -1;
        }        

        return finalniUhel;
    }

    @Override
    public void lineChanged(LineEdge movedEdge)
    {
        if(movedEdge.getOppositeEdge() == edgeToOwner)
        {
            edgeToOwner.setX(getMidX());
            edgeToOwner.setY(getMidY());
        }
        else
        {
            computeLineIntersectionPoint(movedEdge.getOppositeEdge());
        }
    }

    /**
     * Nastavuje vazbu (caru) vedouci k objektu poskytujici
     * toto rozhrani. 
     * 
     * @param myEdgeOfOwnerLine reference na ten konec vazby,
     * ktery prislusi tomuto objektu
     */
    public void setOwnerEdge(LineEdge myEdgeOfOwnerLine)
    {
        this.edgeToOwner = myEdgeOfOwnerLine;
        this.edgeToOwner.setX(getMidX());
        this.edgeToOwner.setY(getMidY());
    }
    
    @Override
	public void setViewMode(ViewModeType newViewMode) 
    {
    	if(newViewMode == ViewModeType.MINIMAL)
    	{
    		this.innerSize = COMPRESED_SIZE;
    	}
    	else if(newViewMode == ViewModeType.STANDARD)
    	{
    		this.innerSize = DEFAULT_SIZE;
    	}
    	else
    	{
    		this.innerSize = DETAIL_SIZE;
    	}
		
    	this.viewMode = newViewMode;
    	recomputeInnerCordDif();
	}

    @Override	
    public void drawObject(Graphics2D g2)
    {
        if(isSelected())
        {
            g2.setColor(Configuration.getColorOfSelected());
        }
        else
        {
            g2.setColor(Color.white);
        }
        // zakryje cary vedouci do stredu rozhrani
        g2.fillOval(this.getX() + innerCordDif,this.getY() + innerCordDif, innerSize, innerSize);

        if(this.isMarked())
        {
        	g2.setColor(Configuration.getColorOfMarked());        	
        }
        else
        {
        	g2.setColor(Color.black);
        }
        // vykresluje kruh reprezentujici rozhrani
        g2.drawOval(this.getX() + innerCordDif,this.getY() + innerCordDif, innerSize, innerSize);

        g2.setColor(Color.black);

        // vykresli packy pro vsechny pripojene objekty
        for(LineEdge edge : attachedEdges)
        {
            drawConnection(g2, edge);
        }
    }

    /**
     * Vypocte pro zadanou caru prusecik s kruznici, na ktere
     * jsou vykreslovany prichytne packy vazeb a prijatemu
     * konci cary nastavi souradnice pruseciku.
     * 
     * @param edge konec cary (vyzby) pro kterou je pocitan prusecik
     */
    private void computeLineIntersectionPoint(LineEdge edge)
    {
        // vypocte delku usecky, cary spojujici dva objekty
        double dist = Math.sqrt(Math.pow((edge.getOppositeEdge().getX() - getMidX() ), 2) +
                Math.pow((edge.getOppositeEdge().getY() - getMidY() ), 2));

        // vypocte koeficient k ze smernicove rovnice primky: y = kx + q
        double kX = (edge.getOppositeEdge().getX() - getMidX()) / dist;
        double kY = (edge.getOppositeEdge().getY() - getMidY()) / dist;
        // z rovnice primky vypocte souradnice bodu protinajici kruznici s polomerem 'width/2'
        double intersectX = kX * width / 2 + getMidX();
        double intersectY = kY * width / 2 + getMidY();
        // vypoctene souradnice nastavi nasemu konci spojovaci cary (usecky)
        edge.setX( (int) intersectX);
        edge.setY( (int) intersectY);
    }

    /**
     * Vykresli packu (pripojeni cary k rozhrani - vazba pouziti rozhrani)
     * pro caru jejiz konec je prijat jako parametr. 
     * 
     * @param g2 reference na objekt grafiky0
     * @param edge konec cary jejich packa je vykreslovana
     */
    private void drawConnection(Graphics2D g2, LineEdge edge)
    {
        // vypocte delku usecky, cary spojujici dva objekty
        double dist = Math.sqrt(Math.pow((edge.getOppositeEdge().getX() - getMidX() ), 2) +
                Math.pow((edge.getOppositeEdge().getY() - getMidY() ), 2));
        // vypocte koeficient k ze smernicove rovnice primky: y = kx + q
        double kX = (edge.getOppositeEdge().getX() - getMidX()) / dist;
        double kY = (edge.getOppositeEdge().getY() - getMidY()) / dist;
        // z rovnice primky vypocte souradnice bodu protinajici kruznici s polomerem 'width/2'
        double xxx = kX * width / 2 + getMidX();
        double yyy = kY * width / 2 + getMidY();

        // vypocte uhel pripojne cary vhledem ke kruznici
        double computedAngle = computeAngleOfPoint(xxx, yyy, g2);
        if( Double.isNaN(computedAngle) )
        {
            // pokud vypocet uhlu selhal hleda se relativni pozice objektu vuci rozhrani
            PositionCoordinates pos = findRelativePosition(edge.getOppositeEdge().getEdgeOwner());

            if (pos == PositionCoordinates.NORTH_EAST
                    || pos == PositionCoordinates.NORTH_WEST
                    || pos == PositionCoordinates.SOUTH_EAST
                    || pos == PositionCoordinates.SOUTH_WEST)
            {
                /*
                 * Vypocet uhlu, vraci hodnotu Double.NaN pro uhly, ktere jsou
                 * nasobky Pi/4. Pokud je tedy nasledne relativni pozice vracena
                 * (v uhlu, ktery je nasobek Pi/4) je nutne tuto pozici dale zpresnit.
                 */
                pos = chooseBetterSide(pos, edge);
            }
            // podle zjistene pozice nastavi uhel vzhledem ke kruznici:
            switch(pos)
            {
                case NORTH:
                    drawHandler(g2, 90);
                    break;
                case SOUTH:
                    drawHandler(g2, 270);
                    break;
                case EAST:
                    drawHandler(g2, 0);
                    break;
                default:
                    drawHandler(g2, 180);
                    break;
            }
        }
        else
        {
            int angle = (int) Math.round(computedAngle);
            // pouzije vypocteny uhel pro vykresleni packy
            
            if( edge.getLine().isMarked())
            {
            	g2.setColor(Configuration.getColorOfMarked());
            }
            
            drawHandler(g2, angle);
            g2.setColor(Color.BLACK);
        }
    }

    /**
     * Vrati relativni pozici objektu prijateho jako parametru 
     * vuci tohoto objektu, tj. objektu nad nimz je zavolana
     * tato metoda).
     * @param object objekt jehoz relativni pozice je vypocitavana
     */
    private PositionCoordinates findRelativePosition(Connectable object)
    {
        /*
         * Najde ralativni pozici (svetovou stranu) prijateho objektu od
         * aktualniho objektu a to pomoci Cohen-Sutherlandova algoritmu.
         */
        int cohenId = 0;

        if(this.getMaxX() < object.getMidX())
        {
            cohenId += PositionCoordinates.E;
        }
        else if(this.getX() > object.getMidX())
        {
            cohenId += PositionCoordinates.W;
        }

        if(this.getY() > object.getMidY())
        {
            cohenId += PositionCoordinates.N;
        }
        else if(this.getMaxY() < object.getMidY())
        {
            cohenId += PositionCoordinates.S;
        }

        return PositionCoordinates.getCordByCohen(cohenId);
    }

    /**
     * Zjisti svetovou stranku (S, J, W nebo Z), na kterou je lepsi pripnout
     * konec cary prijate jako parametr, tak aby cary neprochazela skrze
     * objekt. Tato metoda se pouziva jako upresnujici pokud, pokud 
     * pomoci metody {@link ProvidedInterface#findRelativePosition(Connectable)} 
     * je vracena nejednoznacna pozice (SZ, SV, JV nebo JZ) 
     * 
     * @param pos predtim zjistena pozice objektu na druhe komci cary
     * @param attachedEdge umistovany konec cary
     * @return pozice urcijici svetovou stranu
     */
    private PositionCoordinates chooseBetterSide(PositionCoordinates pos, LineEdge attachedEdge)
    {
        /*
         * Algoritmus: vytovrim caru prochazejici stredem tohoto objektu a
         * a rohem objektu, ktery lezi smerem k pripojovanemu objektu. Vypocitam
         * rovnici teto primky a pak zkoumam, jestli stred pripojovaneho objektu
         * lezi pod nebo nad touto carou. Podle toho vyvodim k jakemu docku
         * (stredu strany) je nejlepsi spojnici pripnout.
         */
        // rohovy body
        int x2, y2;
        int x1, y1;

        switch (pos)
        {
            case NORTH_EAST:
                x2 = this.getMaxX();
                y2 = this.getY();
                x1 = x2 - 1;
                y1 = y2 + 1;
                break;
            case NORTH_WEST:
                x2 = this.getX();
                y2 = this.getY();
                x1 = x2 + 1;
                y1 = y2 + 1;
                break;
            case SOUTH_EAST:
                x2 = this.getMaxX();
                y2 = this.getMaxY();
                x1 = x2 - 1;
                y1 = y2 - 1;
                break;
            default:
                x2 = this.getX();
                y2 = this.getMaxY();
                x1 = x2 + 1;
                y1 = y2 - 1;
                break;
        }


        // parametry obecne rovnice primky
        int a = y1 - y2;
        int b = x2 - x1;
        int c = -(a * x1 + b * y1);

        double yOnLine = (a * attachedEdge.getOppositeEdge().getEdgeOwner().getMidX() + c) / (-1.0 * b);
        boolean isAboveLine = (yOnLine > attachedEdge.getOppositeEdge().getEdgeOwner().getMidY());

        switch (pos)
        {
            case NORTH_EAST:
                if(isAboveLine) return PositionCoordinates.NORTH;
                else return PositionCoordinates.EAST;
            case NORTH_WEST:
                if(isAboveLine) return PositionCoordinates.NORTH;
                else return PositionCoordinates.WEST;
            case SOUTH_EAST:
                if(isAboveLine) return PositionCoordinates.EAST;
                else return PositionCoordinates.SOUTH;
            default:
                if(isAboveLine) return PositionCoordinates.WEST;
                else return PositionCoordinates.SOUTH;
        }
    }

    /**
     * Vykresli packu pripojene vazby
     * 
     * @param g2 graficky objekt
     * @param angle uhel, na ktere se nachazi stred packy
     * vzhledem ke kruznici, na kterou jsou packy vykreslovany
     */
    private void drawHandler(Graphics2D g2, int angle)
    {
        int startAngle = angle - DOCK_SIZE;
        // vykresli packu pripojeni
        g2.drawArc(getX(), getY(), width, height, startAngle,
                2 * DOCK_SIZE );
    }
}
